/********************************************************************************
  * @file    projectstructuretab.cpp
  * @author  Lun Li
  * @version V2.2.0
  * @date    2022.7.8
  * @brief   This file provides all the ProjectStructureTab functions.
  ******************************************************************************/

#include "projecttabwidget.h"
#include "signalconnection.h"

#include <QApplication>
#include <QThread>

ProjectTabWidget::ProjectTabWidget(ProgramConfig *config, QWidget *parent)
    : QTabWidget(parent),
      curProject(nullptr),
      m_config(config),
      compProc(new CompileProcess(config)),
      waveProc(new WaveProcess(config)),
      compThread(new QThread(this)),
      waveThread(new QThread(this))
{
    initializeActionList();
    setTabPosition(South);
    projectFileTab = new ProjectFileTab(this);
    projectStructureTab = new ProjectStructureTab(this);
    addTab(projectFileTab, tr("File"));
    addTab(projectStructureTab, tr("Structure"));

    compProc->moveToThread(compThread);
    waveProc->moveToThread(waveThread);

    connect(projectFileTab, &ProjectFileTab::doubleClicked, this, &ProjectTabWidget::on_projectTreeItemDoubleClicked);
    connect(projectFileTab, &ProjectFileTab::customContextMenuRequested, this, &ProjectTabWidget::on_projectTreeMenuRequested);
    connect(projectStructureTab, &ProjectStructureTab::hierarchyItemDoubleClicked, this, &ProjectTabWidget::fileNeedOpened);
    connect(projectStructureTab, &ProjectStructureTab::addingVariableRequested, this, &ProjectTabWidget::on_addingVariableRequested);

    connect(compProc, &CompileProcess::compileStarted, this, [&] {
        compileActionTeam.tempDisable();
        actionList()[ProjectTabWidget::StopCompile]->setEnabled(true);
    });
    connect(compProc, &CompileProcess::compileFinished, this, [&](const QList<HierarchyModel *> &models) {
        compileActionTeam.restore();
        actionList()[ProjectTabWidget::StopCompile]->setEnabled(false);

        if (compThread->isRunning()) {
            compThread->exit();
        }
        for (HierarchyModel *model : models) {
            projectStructureTab->addModel(model);
        }
        on_projectTreeCurrentChanged(projectFileTab->currentIndex());
    });
    connect(compProc, &CompileProcess::VVPStarted, this, [&] {
        compileActionTeam.tempDisable();        
        actionList()[ProjectTabWidget::StopCompile]->setEnabled(true);
    });
    connect(compProc, &CompileProcess::VVPFinished, this, [&] {
        compileActionTeam.restore();
        actionList()[ProjectTabWidget::StopCompile]->setEnabled(false);
        compThread->quit();
        on_projectTreeCurrentChanged(projectFileTab->currentIndex());
    });
    connect(this, &ProjectTabWidget::projectOpened, this, [&] {
        compProc->setWorkingDirectory(curProject->compilePath());
        waveProc->setWorkingDirectory(curProject->compilePath());
        projectActionTeam.setEnabled(true);
    });
    connect(this, &ProjectTabWidget::projectClosed, this, [&] {
        compProc->closeLog();
        waveProc->closeLog();
        projectActionTeam.setEnabled(false);
    });
    connect(waveProc, &WaveProcess::waveFinished, this, [&] {
        waveThread->quit();
        if (!curProject) {
            return;
        }
        int count = curProject->topItem()->rowCount();
        for (int i = 0; i < count; ++i) {
            curProject->topItem()->child(i)->setIsWaveOpening(false);
        }
    });

    extern SignalConnection globalSignal;
    connect(this, &ProjectTabWidget::compileFilesRequested, &globalSignal, &SignalConnection::compileFilesRequested);
    connect(this, &ProjectTabWidget::runFileRequested, &globalSignal, &SignalConnection::runFileRequested);
    connect(this, &ProjectTabWidget::compileCommandRequested, &globalSignal, &SignalConnection::compileCommandRequested);
    connect(this, &ProjectTabWidget::showWaveRequested, &globalSignal, &SignalConnection::showWaveRequested);
    connect(this, &ProjectTabWidget::addWaveCommandRequested, &globalSignal, &SignalConnection::addWaveCommandRequested);
    connect(this, &ProjectTabWidget::fileNeedOpened, &globalSignal, &SignalConnection::fileNeedOpened);
    connect(this, &ProjectTabWidget::fileNeedClosed, &globalSignal, &SignalConnection::fileNeedClosed);
    connect(this, &ProjectTabWidget::fileNeedSaved, &globalSignal, &SignalConnection::fileNeedSaved);
    connect(this, &ProjectTabWidget::projectOpened, &globalSignal, &SignalConnection::projectOpened);
    connect(this, &ProjectTabWidget::projectClosed, &globalSignal, &SignalConnection::projectClosed);
    connect(this, &ProjectTabWidget::showErrorLineRequested, &globalSignal, &SignalConnection::showErrorLineRequested);

    connect(&globalSignal, &SignalConnection::errorItemClicked, this, [=](const QString &filePath, int lineNumber) {
        Source *file = fileFromPath(filePath);
        if (!file) {
            return;
        }
        emit showErrorLineRequested(file, lineNumber);
    });
    connect(&globalSignal, &SignalConnection::compileCmdIsInput, this, &ProjectTabWidget::runCompileCmd);
    connect(&globalSignal, &SignalConnection::waveCmdIsInput, this, &ProjectTabWidget::runWaveCmd);

    m_originalEnv = qEnvironmentVariable("PATH");
    connect(m_config, &ProgramConfig::toolConfigChanged, this, [&] {
        QString path = QDir::toNativeSeparators(m_config->compileToolPath()) + ";" +
                QDir::toNativeSeparators(m_config->waveToolPath()) + ";" + m_originalEnv;
        qputenv("PATH", path.toStdString().c_str());
    });
}

ProjectTabWidget::~ProjectTabWidget()
{
    compProc->kill();
    compProc->waitForFinished();
    waveProc->kill();
    waveProc->waitForFinished();
    compThread->terminate();
    waveThread->terminate();
    compProc->deleteLater();
    waveProc->deleteLater();
}

const QList<QAction *> &ProjectTabWidget::actionList() const
{
    return m_actionList;
}

Source *ProjectTabWidget::fileFromPath(const QString &path)
{
    if (!curProject) {
        return nullptr;
    }
    for (Source *file : curProject->fileList()) {
        if (QDir(file->filePath()) == QDir(QDir(curProject->compilePath()).absoluteFilePath(path))) {
            return file;
        }
    }
    return nullptr;
}

void ProjectTabWidget::openProject(const QString &projectFilePath)
{
    if (curProject) {
        on_closeProjectButtonClicked();
    }
    if (curProject) {
        return;
    }
    QSettings *projectFile = new QSettings(projectFilePath, QSettings::IniFormat);
    QString projectName = projectFile->value("Properties/projectName").toString();
    QString projectPath = QFileInfo(projectFilePath).absolutePath();
    curProject = new Project(projectName, projectPath, projectFile, this);
    if (projectFileTab->selectionModel()) {
        projectFileTab->selectionModel()->deleteLater();
    }
    projectFileTab->setModel(curProject);
    connect(projectFileTab->selectionModel(), &QItemSelectionModel::currentChanged, this, &ProjectTabWidget::on_projectTreeCurrentChanged);
    projectFileTab->expandAll();
    QString path = QDir::toNativeSeparators(m_config->compileToolPath()) + ";" +
            QDir::toNativeSeparators(m_config->waveToolPath()) + ";" + m_originalEnv;
    qputenv("PATH", path.toStdString().c_str());
    emit projectOpened();
}

void ProjectTabWidget::addSource(const QString &sourcePath)
{
    if (!curProject) {
        return;
    }
    QString sourceName = QFileInfo(sourcePath).fileName();
    for (Source *file : curProject->fileList()) {
        if (file->fileName() == sourceName) {
            int ret = QMessageBox::question(this, tr("Add Source"), tr("File \"%1\" is already in project.\n"
                                                                       "Redirect to \"%2\"?").arg(sourceName, sourcePath));
            if (ret == QMessageBox::Yes) {
                file->setFileName(sourcePath);
            }
            return;
        }
    }
    Source *file = new Source(sourcePath, false, this);
    if (!QDir(curProject->sourcePath()).exists(sourceName)) {
        QString newPath = QDir(curProject->sourcePath()).absoluteFilePath(sourceName);
        if (!QFile::copy(sourcePath, newPath)) {
            QMessageBox::warning(this, tr("Add Source"), tr("Cannot copy\n"
                                                            "%1\n"
                                                            "to\n"
                                                            "%2.\n"
                                                            "Source will remain in its own directory"));
        } else {
            file->setFileName(newPath);
        }
    }
    curProject->addSource(file);
}

void ProjectTabWidget::compileSources(QList<Source *> files, bool merge)
{
    if (!curProject) {
        return;
    }
    for (Source *file : curProject->fileList()) {
        emit fileNeedSaved(file);
    }
    compThread->start();
    emit compileFilesRequested(files, curProject, merge);
}

void ProjectTabWidget::runCompileCmd(const QString &command)
{    
    compThread->start();
    emit compileCommandRequested(command);
}

void ProjectTabWidget::runWaveCmd(const QString &command)
{
    waveThread->start();
    emit addWaveCommandRequested(command);
}

void ProjectTabWidget::on_newProjectButtonClicked()
{
    NewProjectWizard *wizard = new NewProjectWizard(m_config);
    connect(wizard, &NewProjectWizard::wizardFinished, this, &ProjectTabWidget::openProject);
    wizard->show();
}

void ProjectTabWidget::on_openProjectButtonClicked()
{
    QString path = QFileDialog::getOpenFileName(this, tr("Select Project File"), m_config->lastOpenPath());
    if (path.isEmpty()) {
        return;
    }
    m_config->setLastOpenPath(path);
    openProject(path);
}

void ProjectTabWidget::on_closeProjectButtonClicked()
{
    if (!curProject) {
        return;
    }
    for (Source *file : curProject->fileList()) {
        emit fileNeedClosed(file);
        if (file->indexInEditor() >= 0) {
            return;
        }
    }
    projectFileTab->clearSelection();
    curProject->deleteLater();
    curProject = nullptr;
    emit projectClosed();
}

void ProjectTabWidget::on_newSourceButtonClicked()
{
    if (!curProject) {
        return;
    }
    NewSourceWizard *wizard = new NewSourceWizard(curProject);
    connect(wizard, &NewSourceWizard::wizardFinished, this, &ProjectTabWidget::addSource);
    wizard->show();
}

void ProjectTabWidget::on_addSourceButtonClicked()
{
    QStringList sourcePaths = QFileDialog::getOpenFileNames(this, tr("Add Sources"), m_config->lastOpenPath());
    if (sourcePaths.isEmpty()) {
        return;
    }
    m_config->setLastOpenPath(sourcePaths.at(0));
    for (const QString &path : sourcePaths) {
        addSource(path);
    }
}

void ProjectTabWidget::on_openSourceButtonClicked()
{
    QModelIndex index = projectFileTab->currentIndex();
    if (!index.isValid()) {
        return;
    }
    emit fileNeedOpened(curProject->itemFromIndex(index)->file());
}

void ProjectTabWidget::on_removeSourceButtonClicked()
{
    QModelIndex index = projectFileTab->currentIndex();
    if (!index.isValid()) {
        return;
    }
    for (HierarchyModel *model : curProject->itemFromIndex(index)->hierModels()) {
        projectStructureTab->removeModel(model);
    }
    Source *file = curProject->itemFromIndex(index)->file();
    emit fileNeedClosed(file);
    if (file->indexInEditor() >= 0) {
        return;
    }
    curProject->removeSource(file);
}

void ProjectTabWidget::on_compileButtonClicked()
{
    QModelIndex index = projectFileTab->currentIndex();
    if (!index.isValid()) {
        return;
    }
    projectFileTab->expand(index);
    compileSources(QList<Source *>() << curProject->itemFromIndex(index)->file());
}

void ProjectTabWidget::on_compileAllButtonClicked()
{
    if (!curProject) {
        return;
    }
    projectFileTab->expandAll();
    compileSources(curProject->fileList());
}

void ProjectTabWidget::on_compileProjectButtonClicked()
{
    if (!curProject) {
        return;
    }
    projectFileTab->expandAll();
    compileSources(curProject->fileList(), true);
}

void ProjectTabWidget::on_runButtonClicked()
{
    if (!curProject) {
        return;
    }
    QModelIndex index = projectFileTab->currentIndex();
    if (!index.isValid()) {
        return;
    }
    Source *file = curProject->itemFromIndex(index)->file();
    compThread->start();
    projectFileTab->expand(index);
    emit runFileRequested(file, curProject);
}

void ProjectTabWidget::on_stopCompileButtonClicked()
{
    compThread->requestInterruption();
    emit compProc->compileFinished(QList<HierarchyModel *>());
    compProc = new CompileProcess(m_config);
    compThread = new QThread(this);
    compProc->moveToThread(compThread);
    connect(compProc, &CompileProcess::compileStarted, this, [&] {
        compileActionTeam.tempDisable();
    });
    connect(compProc, &CompileProcess::compileFinished, this, [&](const QList<HierarchyModel *> &models) {
        compileActionTeam.restore();
        if (compThread->isRunning()) {
            compThread->exit();
        }
        for (HierarchyModel *model : models) {
            projectStructureTab->addModel(model);
        }
        on_projectTreeCurrentChanged(projectFileTab->currentIndex());
    });
    connect(compProc, &CompileProcess::VVPStarted, this, [&] {
        compileActionTeam.tempDisable();
    });
    connect(compProc, &CompileProcess::VVPFinished, this, [&] {
        compileActionTeam.restore();
        compThread->quit();
        on_projectTreeCurrentChanged(projectFileTab->currentIndex());
    });
    emit projectOpened();
}

void ProjectTabWidget::on_showWaveButtonClicked()
{
    QModelIndex index = projectFileTab->currentIndex();
    if (!index.isValid()) {
        return;
    }
    QString waveFilePath = curProject->itemFromIndex(index)->file()->waveFilePath();
    if (QDir(QFileInfo(waveFilePath).absolutePath()) == QDir(curProject->compilePath())) {
        waveFilePath = QDir(curProject->compilePath()).relativeFilePath(waveFilePath);
    }
    if (waveThread->isRunning()) {
        QString command = QString("gtkwave::loadFile \"%1\"").arg(waveFilePath);
        emit addWaveCommandRequested(command);
    } else {
        QString command = QString("gtkwave -W %1").arg(waveFilePath);
        waveThread->start();
        emit showWaveRequested(command);
    }
    curProject->itemFromIndex(index)->setIsWaveOpening(true);
}

void ProjectTabWidget::initializeActionList()
{
    m_actionList.resize(ShowWave + 1);

    m_actionList[NewProject] = new QAction(QIcon(":/icon/NewProject.png"), tr("New Project"), this);
    connect(m_actionList[NewProject], &QAction::triggered, this, &ProjectTabWidget::on_newProjectButtonClicked);

    m_actionList[OpenProject] = new QAction(QIcon(":/icon/OpenProject.png"), tr("Open Project"), this);
    connect(m_actionList[OpenProject], &QAction::triggered, this, &ProjectTabWidget::on_openProjectButtonClicked);

    m_actionList[CloseProject] = new QAction(QIcon(":/icon/CloseProject.png"), tr("Close Project"), this);
    connect(m_actionList[CloseProject], &QAction::triggered, this, &ProjectTabWidget::on_closeProjectButtonClicked);

    m_actionList[NewSource] = new QAction(tr("New Source"), this);
    connect(m_actionList[NewSource], &QAction::triggered, this, &ProjectTabWidget::on_newSourceButtonClicked);

    m_actionList[AddSource] = new QAction(tr("Add Source"), this);
    connect(m_actionList[AddSource], &QAction::triggered, this, &ProjectTabWidget::on_addSourceButtonClicked);

    m_actionList[OpenSource] = new QAction(tr("Open Source"), this);
    connect(m_actionList[OpenSource], &QAction::triggered, this, &ProjectTabWidget::on_openSourceButtonClicked);

    m_actionList[RemoveSource] = new QAction(tr("Remove Source"), this);
    connect(m_actionList[RemoveSource], &QAction::triggered, this, &ProjectTabWidget::on_removeSourceButtonClicked);

    m_actionList[Compile] = new QAction(QIcon(":/icon/CompileFile.png"), tr("Compile"), this);
    connect(m_actionList[Compile], &QAction::triggered, this, &ProjectTabWidget::on_compileButtonClicked);

    m_actionList[CompileAll] = new QAction(tr("Compile All"), this);
    connect(m_actionList[CompileAll], &QAction::triggered, this, &ProjectTabWidget::on_compileAllButtonClicked);

    m_actionList[CompileProject] = new QAction(tr("Compile Project"), this);
    connect(m_actionList[CompileProject], &QAction::triggered, this, &ProjectTabWidget::on_compileProjectButtonClicked);

    m_actionList[StopCompile] = new QAction(tr("Stop Compile"), this);
    connect(m_actionList[StopCompile], &QAction::triggered, this, &ProjectTabWidget::on_stopCompileButtonClicked);

    m_actionList[Run] = new QAction(QIcon(":/icon/RunFile.png"), tr("Run"), this);
    connect(m_actionList[Run], &QAction::triggered, this, &ProjectTabWidget::on_runButtonClicked);

    m_actionList[ShowWave] = new QAction(tr("Show Wave"), this);
    connect(m_actionList[ShowWave], &QAction::triggered, this, &ProjectTabWidget::on_showWaveButtonClicked);

    projectActionTeam << m_actionList[CloseProject]
                       << m_actionList[NewSource]
                       << m_actionList[AddSource]
                       << m_actionList[CompileAll]
                       << m_actionList[CompileProject];
    projectContextMenu = new QMenu(this);
    projectContextMenu->addActions(projectActionTeam);

    fileActionTeam << m_actionList[OpenSource]
                    << m_actionList[RemoveSource]
                    << m_actionList[Compile]
                    << m_actionList[Run]
                    << m_actionList[ShowWave];
    fileContextMenu = new QMenu(this);
    fileContextMenu->addActions(fileActionTeam);

    compileActionTeam << m_actionList[NewProject]
                       << m_actionList[OpenProject]
                       << m_actionList[CloseProject]
                       << m_actionList[NewSource]
                       << m_actionList[AddSource]
                       << m_actionList[RemoveSource]
                       << m_actionList[Compile]
                       << m_actionList[CompileAll]
                       << m_actionList[CompileProject]
                       << m_actionList[Run];

    projectActionTeam.setEnabled(false);
    fileActionTeam.setEnabled(false);
    m_actionList[StopCompile]->setEnabled(false);
}

void ProjectTabWidget::on_projectTreeCurrentChanged(const QModelIndex &index)
{
    if (!index.isValid()) {
        return;
    }
    switch (curProject->itemFromIndex(index)->itemType()) {
    case ProjectItem::Project:
    case ProjectItem::Intermediate:
    case ProjectItem::WaveFile:
        fileActionTeam.setEnabled(false);
        break;
    case ProjectItem::NoCompiled:
    case ProjectItem::ErrorCompiled:
    case ProjectItem::Compiled:
    case ProjectItem::ErrorRun:
    case ProjectItem::Run:
        fileActionTeam.setEnabled(true);
        m_actionList[Run]->setEnabled(!curProject->itemFromIndex(index)->file()->compiledFilePath().isEmpty());
        m_actionList[ShowWave]->setEnabled(!curProject->itemFromIndex(index)->file()->waveFilePath().isEmpty());
        break;
    }
}

void ProjectTabWidget::on_projectTreeItemDoubleClicked(const QModelIndex &index)
{
    if (!index.isValid()) {
        return;
    }
    emit fileNeedOpened(curProject->itemFromIndex(index)->file());
}

void ProjectTabWidget::on_projectTreeMenuRequested(const QPoint &pos)
{
    QModelIndex index = projectFileTab->indexAt(pos);
    if (!index.isValid()) {
        return;
    }
    switch (curProject->itemFromIndex(index)->itemType()) {
    case ProjectItem::Project:
        projectContextMenu->exec(mapToGlobal(pos));
        break;
    case ProjectItem::NoCompiled:
    case ProjectItem::ErrorCompiled:
    case ProjectItem::Compiled:
    case ProjectItem::ErrorRun:
    case ProjectItem::Run:
        fileContextMenu->exec(mapToGlobal(pos));
        break;
    default:
        break;
    }
}

void ProjectTabWidget::on_addingVariableRequested(const QString &name)
{
    QString command = QString("set num_added [ gtkwave::addSignalsFromList \"%1\" ]").arg(name);
    emit addWaveCommandRequested(command);
}
